//
//  ErrorHandling.swift
//  cnswift-demo
//
//  Created by YangWei on 2021/1/2.
//  Copyright © 2021 YangWei. All rights reserved.
//

import UIKit

class ErrorHandling: SWDBaseObject {
    
    class func run() {
        
        let errorHandling = ErrorHandling()
        
        // 1
        errorHandling.representingAndThrowingErrors()
        
        // 2.1
        errorHandling.propagatingErrorsUsingThrowingFunctions()
        
        // 2.2
        errorHandling.handlingErrorsUsingDoCatch()
        
        // 2.3
        errorHandling.convertingErrorsToOptionalValues()
        
        // 2.4
        errorHandling.disablingErrorPropagation()
        
        // 3
        errorHandling.specifyingCleanupActions()
        
    }
    
    /// 1 表示和抛出错误
    func representingAndThrowingErrors() {
        let title = "1 表示和抛出错误";
        printStartLine(title)
        
        // 抛出一个错误，提示贩卖机还需要 5 个硬币
//        throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
        
        printEndLine(title)
    }
    
    /// 2.1 用 throwing 函数传递错误
    func propagatingErrorsUsingThrowingFunctions() {
        let title = "2.1 用 throwing 函数传递错误";
        printStartLine(title)
        
        // 查看定义：Item、VendingMachine、favoriteSnacks、buyFavoriteSnack、PurchasedSnack
        
        printEndLine(title)
    }
    
    /// 2.2 用 Do-Catch 处理错误
    func handlingErrorsUsingDoCatch() {
        let title = "2.2 用 Do-Catch 处理错误";
        printStartLine(title)
        
        /* 处理 VendingMachineError 枚举类型的全部三种情况 */
        
        // 创建一个售货机
        let vendingMachine = VendingMachine()
        // 投 8 个币
        vendingMachine.coinsDeposited = 8
        do {
            // 给 爱丽丝 买炸薯条，炸薯条当前金额为10，有4个
            try buyFavoriteSnack(person: "爱丽丝", vendingMachine: vendingMachine)
            print("✌️ 购买成功")
        } catch VendingMachineError.invalidSelection {
            print("❌ 选择不正确")
        } catch VendingMachineError.outOfStock {
            print("❌ 没货了")
        } catch VendingMachineError.insufficientFunds(let coinsNeeded) {
            print("❌ 投入钱不够. 再投 \(coinsNeeded) 个币")
        } catch {
            // 错误没有被匹配，被最后一个 catch 语句捕获，并赋值给一个 error 常量
            print("未知错误: \(error).")
        }

        // 如果所有 catch 子句都未处理错误，错误就会传递到周围的作用域。
        // 错误必须要被某个周围的作用域处理的
        
        
        /* 不是 VendingMachineError 中声明的错误会在调用函数的地方被捕获 */
        func nourish(with item: String) throws {
            do {
                try vendingMachine.vend(itemNamed: item)
            } catch is VendingMachineError {
                // 是 VendingMachineError 类型的错误，打印一条消息
                print("发生错误，不能从那台售货机上购买.")
            }
        }
        
        do {
            try nourish(with: "Beet-Flavored Chips")
        } catch {
            // 捕获通用错误
            print("未知错误 非售货机相关错误: \(error)")
        }
        
        
        /* 另一种捕获多个相关错误的方式，将它们放在 catch 后，通过逗号分隔 */
        
        /// 捕获列出来的 VendingMachine 错误，并打印信息
        /// 其他错误会传递到外面的作用域，包括以后可能增加的其他 VendingMachine 错误
        func eat(item: String) throws {
            do {
                try vendingMachine.vend(itemNamed: item)
                // 使用逗号（,）分割多个相关错误
            } catch VendingMachineError.invalidSelection, VendingMachineError.insufficientFunds, VendingMachineError.outOfStock {
                print("不可选择, 没有库存, 或者钱不够")
            }
        }
        
        printEndLine(title)
    }
    
    /// 2.3 将错误转换为可选值
    func convertingErrorsToOptionalValues() {
        let title = "2.3 将错误转换为可选值";
        printStartLine(title)
        
        // 如果 someThrowingFunction() 抛出一个错误，x 和 y 的值是 nil
        // 否则 x 和 y 的值就是该函数的返回值
        let x = try? someThrowingFunction()
        print(x as Any)

        let y: Int?
        do {
            y = try someThrowingFunction()
        } catch {
            y = nil
        }
        print(y as Any)
        
        
        /* 对所有的错误都采用同样的方式来处理，用 try? 写出简洁的错误处理代码。*/
        // 查看定义：func fetchData() -> Data?
        
        printEndLine(title)
    }
    
    /// 2.4 禁用错误传递
    func disablingErrorPropagation() {
        let title = "2.4 禁用错误传递";
        printStartLine(title)
        
        // 使用 loadImage(atPath:) 函数，该函数从给定的路径加载图片资源，如果图片无法载入则抛出一个错误
        // 在这种情况，图片和应用绑定，运行时不会有错误抛出，适合禁用错误传递。
        let photo = try! loadImage("./Resources/John Appleseed.jpg")
        print(photo)
        
        printEndLine(title)
    }
    
    /// 3 指定清理操作
    func specifyingCleanupActions() {
        let title = "3 指定清理操作";
        printStartLine(title)
        
        // 查看定义：func processFile(filename: String) throws
        
        printEndLine(title)
    }
}

/// 1 表示和抛出错误
/// 在游戏中操作自动贩卖机时，表示可能会出现的错误状态
enum VendingMachineError: Error {
    case invalidSelection                        //选择无效
    case insufficientFunds(coinsNeeded: Int)     //金额不足
    case outOfStock                              //缺货
}

/// 2.1 用 throwing 函数传递错误
struct Item {
    var price: Int
    var count: Int
}

/// 2.1 用 throwing 函数传递错误
/// 售货机
class VendingMachine {
    // 库存
    var inventory = [
        // 糖果棒
        "糖果棒": Item(price: 12, count: 7),
        // 炸薯条
        "炸薯条": Item(price: 10, count: 4),
        // 椒盐脆饼
        "椒盐脆饼": Item(price: 7, count: 11)
    ]
    // 硬币存放
    var coinsDeposited = 0
    
    // 请求的物品不存在、缺货或投入金额小于物品价格，抛出一个相应的 VendingMachineError
    func vend(itemNamed name: String) throws {
        
        print("【售货机】：......开始处理购买\(name) ")
        
        // 使用 guard 确保购买物品有任一条件不满足时，提前退出方法并抛出错误
        guard let item = inventory[name] else {
            print("【售货机】：......检查商品\(name)不存在 ❌")
            throw VendingMachineError.invalidSelection
        }
        
        print("【售货机】：......检查商品\(name)存在 ✅")
        
        guard item.count > 0 else {
            print("【售货机】：......检查商品\(name) 数量为\(item.count)，库存不足❌")
            throw VendingMachineError.outOfStock
        }
        
        print("【售货机】：......检查商品\(name) 数量为\(item.count) ✅")
        
        guard item.price <= coinsDeposited else {
            print("【售货机】：......检查收款 商品价格：\(item.price)，投币数量：\(coinsDeposited)，投币金额不足❌")
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        print("【售货机】：......检查收款 价格：\(item.price)，投币数量：\(coinsDeposited)，金额充足✅")
        
        coinsDeposited -= item.price
        print("【售货机】：......核销收款：✅，剩余投币数量：\(coinsDeposited)")
        
        var newItem = item
        newItem.count -= 1
        print("【售货机】：......\(name)出库 ✅ 剩余库存数量为\(item.count)")
        inventory[name] = newItem
        
        print("【售货机】：.... 正在配置商品 \(name) 出货✅")
    }
}

/// 2.1 用 throwing 函数传递错误
/// 喜欢的零食
let favoriteSnacks = [
    "爱丽丝": "炸薯条",
    "鲍勃": "甘草", // 甘草
    "艾文": "椒盐脆饼",
]

/// 2.1 用 throwing 函数传递错误
/// 查找某人最喜欢的零食，并通过调用 vend(itemNamed:) 方法来尝试为他们购买
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    // buyFavoriteSnack 是 throwing 函数，内部调用的 vend(itemNamed:) 也是 throwing 函数，最终将异常向上传递到 buyFavoriteSnack 函数调用的地方
    print("\(person) 要买东西了")
    let snackName = favoriteSnacks[person] ?? "糖果棒"
    try vendingMachine.vend(itemNamed: snackName)
}

/// 2.1 用 throwing 函数传递错误
/// 购买零食
struct PurchasedSnack {
    let name: String
    // throwing 构造器
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}

/// 2.3 将错误转换为可选值
func someThrowingFunction() throws -> Int {
    // ...
    return 1
}

/// 2.3 将错误转换为可选值
func fetchDataFromDisk() throws -> Data {
    // ...
    return Data()
}

/// 2.3 将错误转换为可选值
func fetchDataFromServer() throws -> Data {
    // ...
    return Data()
}

/// 2.3 将错误转换为可选值
func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() { return data }
    if let data = try? fetchDataFromServer() { return data }
    return nil
}

/// 2.4 禁用错误传递
func loadImage(_ url: String) throws {
//    throw VendingMachineError.invalidSelection
}

/// 3 指定清理操作
func processFile(filename: String) throws {
    /*
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)
        }
        while let line = try file.readline() {
            // 处理文件。
        }
        // close(file) 会在这里被调用，即作用域的最后。
    }
     */
}
